// 控制器模块，是我们的核心业务逻辑，其下有三个子模块：负载均衡模块，构建网页模块，判断模块
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm> //sort()
#include <mutex>     //std::mutex C++提供的互斥锁
#include <cassert>

#include "oj_model6.hpp" //question4题库（竞赛&&语言入门）交互模块
#include "oj_view4.hpp"  //引入网页渲染模块

#include <jsoncpp/json/json.h> //引入jsoncpp库完成数据的序列化和反序列化
#include "../common/httplib.h" //引入httplib完成网络通信服务
#include "../common/util.hpp"  //引入common目录下的工具模块
#include "../common/log.hpp"   //引入common目录下的日志模块

namespace ns_control4
{
    using namespace std;
    using namespace ns_util;   // 展开工具模块
    using namespace ns_log;    // 展开日志模块
    using namespace ns_model4; // 展开数据交互模块
    using namespace ns_view4;  // 展开网页渲染模块
    using namespace httplib;   // 展开httplib库

    // 提供编译运行服务的主机（启动了编译运行系统的进程，一个进程一个Machine）
    class Machine4
    {
    public:
        std::string ip;  // 提供编译运行服务的主机的ip
        int port;        // 主机的port
        uint64_t load;   // 记录主机的负载，即主机正在处理的任务数量
        std::mutex *mtx; // C++中的mutex禁止拷贝的，所以只能使用指针，以指针的方式让外部对象来能在创建主机对象时初始化这把锁
    public:
        Machine4() : ip(""), port(0), load(0), mtx(nullptr)
        {
        }
        ~Machine4()
        {
        }

    public:
        // 主机负载增加了
        void IncLoad()
        {
            if (mtx)
                mtx->lock();
            ++load;
            if (mtx)
                mtx->unlock();
        }
        // 主机负载减少了
        void DecLoad()
        {
            if (mtx)
                mtx->lock();
            --load;
            if (mtx)
                mtx->unlock();
        }
        // 将主机负载清0
        void ResetLoad()
        {
            if (mtx)
                mtx->lock();
            load = 0;
            if (mtx)
                mtx->unlock();
        }
        // 获取主机当前负载
        uint64_t Load()
        {
            uint64_t _load = 0;
            if (mtx)
                mtx->lock();
            _load = load;
            if (mtx)
                mtx->unlock();

            return _load;
        }
    };

    const std::string service_machine = "./conf/service_machine.conf"; // 主机列表文件的路径
    // 负载均衡模块
    class LoadBalance4
    {
    private:
        std::vector<Machine4> machines; // 主机容器：保存提供编译运行服务的主机对象，vector容器的下标，充当当前主机的id
        std::vector<int> online;        // 在线主机下标容器：保存所有在线的主机id(id即主机在vector容器的下标)
        std::vector<int> offline;       // 离线主机下标容器：保存所有离线的主机id
        // 多个提供编译运行服务的主机都会访问负载均衡模块，有多个执行流，要加锁保证数据安全。
        std::mutex mtx; // 互斥锁，用来保证LoadBlance模块的数据安全，防止出现数据紊乱

    public:
        LoadBalance4()
        {
            // 负载均衡模块启动时，先将所有提供编译运行服务的主机加载进负载均衡模块
            assert(LoadConf(service_machine));
            LOG(INFO) << "加载 " << service_machine << " 成功\n";
        }
        ~LoadBalance4()
        {
        }

    public:
        /*按行读取主机列表文件，切割出主机信息，用主机信息构建主机对象，并加载进vector容器中*/
        bool LoadConf(const std::string &machine_conf)
        {
            std::ifstream in(machine_conf); // 以读方式打开主机列表文件
            if (!in.is_open())              // 如果打开文件失败，
            {
                LOG(FATAL) << " 加载主机列表文件: " << machine_conf << " 失败，系统无法继续运行！\n";
                return false;
            }
            // 1. 按行读取主机列表文件。
            std::string line;
            while (std::getline(in, line))
            {
                // 2. 切割从文件中读出的行内容字符串，将切割出的主机信息保存到输出型参数中
                std::vector<std::string> tokens;             // 输出型参数，用来保存切分出来的主机信息
                StringUtil::SplitString(line, &tokens, ":"); // 调用工具模块中的字符串工具进行字符串切割
                if (tokens.size() != 2)                      // 如果切割失败，跳过本行继续切割。
                {
                    LOG(WARNING) << " 主机列表文件中的主机信息： " << line << " 切分失败，请尽快查看。\n";
                    continue;
                }
                // 3. 靠切割出来的信息实例化主机对象
                Machine4 m;
                m.ip = tokens[0];
                m.port = atoi(tokens[1].c_str());
                m.load = 0;
                m.mtx = new std::mutex(); // 给每个主机对象创建一把锁
                // 4. 更新保存在线主机的vector容器，并且将主机对象保存到vector容器中。
                online.push_back(machines.size()); // 注意，主机数量比主机下标大1所以要先更新保存在线主机的vector容器
                machines.push_back(m);
            }

            in.close(); // 打开文件后要记得关闭。
            return true;
        }

        /*****************************************************************************
         * 使用负载均衡算法智能选择主机提供服务
         * 参数：
         *     id: 输出型参数，返回在线主机下标容器中保存的负载最小主机的在主机容器里的下标
         *     m : 输出型参数，返回负载最小主机对象
         * 常用的两种负载均衡算法（1. 随机数+hash 2. 轮询+hash）
         *****************************************************************************/
        bool SmartChoice(int *id, Machine4 **m)
        {
            mtx.lock(); // 加锁保证数据安全，负载均衡模块会被多个主机访问。
            // 使用轮询的方式实现负载均衡
            int online_num = online.size(); // 获取在线主机的数量
            if (online_num == 0)            // 如果在线主机数量为0，无法进行编译运行服务。
            {
                mtx.unlock();
                LOG(FATAL) << " 后端所有的负责编译服务的主机都已经离线, OJ系统无法继续运行，请运维的同事尽快查看\n";
                return false;
            }
            // 轮询式负载均衡算法的实现（循环+hash）：
            // 通过遍历，找到负载最小的在线主机
            int minid = online[0];                          // 保存负载最小的主机在主机容器里的下标
            *m = &machines[online[0]];                      // 保存负载最小的主机的地址
            uint64_t min_load = machines[online[0]].Load(); // 最小的负载
            for (int i = 1; i < online_num; i++)
            {
                uint64_t curr_load = machines[online[i]].Load(); // 获取当前主机的负载
                if (min_load > curr_load)                        // 比较
                {
                    minid = online[i];
                    min_load = curr_load;
                }
            }
            *id = minid;           // 通过输出型参数，返回负载最小的主机在主机容器里的下标
            *m = &machines[minid]; // 返回负载最小的主机的地址
            mtx.unlock();          // 访问完临界区资源后要解锁
            return true;
        }

        /*离线指定主机，就是从在线主机下标容器删除该元素，在离线主机下标容器增加该元素*/
        void OfflineMachine(int which)
        {
            mtx.lock(); // 离线时可能负载均衡模块正在选择主机，所以要加锁防止两者冲突。
            // 依据主机在主机容器里的下标，到在线主机下标容器里找到该元素
            for (auto iter = online.begin(); iter != online.end(); iter++)
            {
                if (*iter == which) // 已找到该元素：将主机负载清0，从在线主机下标容器删除该元素，在离线主机下标容器增加该元素。
                {
                    machines[which].ResetLoad(); // 将主机负载清0
                    online.erase(iter);          // 删除指定内容元素，从在线主机下标容器中
                    offline.push_back(which);    // 尾插元素，到离线主机下标容器中
                    break;                       // 因为break的存在，所有我们暂时不考虑迭代器失效的问题
                }
            }
            mtx.unlock();
        }
        /*上线所有主机*/
        void OnlineMachine()
        {
            // 我们统一上线，后面统一解决
            mtx.lock();
            online.insert(online.end(), offline.begin(), offline.end());
            offline.erase(offline.begin(), offline.end());
            mtx.unlock();

            LOG(INFO) << "所有的主机有上线啦!"
                      << "\n";
        }
        /*打印在线主机下标列表和离线主机下标列表*/
        void ShowMachines()
        {
            mtx.lock();
            std::cout << "当前在线主机列表: ";
            for (auto &id : online)
            {
                std::cout << id << " ";
            }
            std::cout << std::endl;
            std::cout << "当前离线主机列表: ";
            for (auto &id : offline)
            {
                std::cout << id << " ";
            }
            std::cout << std::endl;
            mtx.unlock();
        }
    };

    // 构建网页模块，判题模块
    class Control4
    {
    private:
        Model4 model_;             // 引入model模块，提供后台数据
        View4 view_;               // 引入view模块，提供html渲染功能
        LoadBalance4 load_blance_; // 引入核心负载均衡模块，每次判题选择出负载最低的主机
    public:
        Control4()
        {
        }
        ~Control4()
        {
        }

    public:
        void RecoveryMachine()
        {
            load_blance_.OnlineMachine();
        }

        // 构建网页模块
        /* 调用model模块和view模块构建网页，将构建好的网页的网页代码通过输出型参数返回给上层。（html: 输出型参数）*/
        bool AllQuestions4(string *html)
        {
            vector<struct Question4> all; // 保存所有描述题目的Question结构体的vector容器
            // 1.调用model模块获取数据
            if (model_.GetAllQuestions(&all))
            {
                // 在容器内部使用sort函数按题目编号进行排序
                sort(all.begin(), all.end(), [](const struct Question4 &q1, const struct Question4 &q2)
                     { return atoi(q1.number.c_str()) < atoi(q2.number.c_str()); });
                // 2.获取题目信息数据成功，依靠数据渲染网页
                view_.AllExpandHtml4(all, html);
            }
            else // 调用model模块获取数据失败
            {
                *html = "形成题目列表失败，题库正在维护中";
                return false;
            }
            return true;
        }
        /* 根据题目编号，调用model模块和view模块构建网页，将构建好的网页的网页代码通过输出型参数返回给上层。（html: 输出型参数）*/
        bool Question4(const string &number, string *html)
        {
            struct Question4 q; // 保存描述题目的Question4结构体
            // 1.调用model模块获取数据
            if (model_.GetOneQuestion(number, &q))
            {
                // 2.获取指定题目信息成功，依靠描述题目的Question4结构体渲染网页
                view_.OneExpandHtml4(q, html);
            }
            else
            {
                *html = "题目: " + number + " 不存在!";
                return false;
            }
            return true;
        }

        /***********************************************************
         * 判题模块
         * 参数：
         *     number : 题号
         *     input  : 上层传来的json串
         *     output : 输出型参数，返回给上层的json串
         ***********************************************************/
        void Judge4(const std::string &number, const std::string in_json, std::string token, std::string *out_json)
        {
            int userid;
            std::string role;
            if (token != "null" && TokenVerify::verify(token, userid, role))
            {
                // 0. 根据题目编号，调用model4模块，直接拿到对应的题目细节
                struct Question4 q;
                model_.GetOneQuestion(number, &q);

                // 1. 反序列化:将Json字符串转化为Json类，从Json类中得到题目的id，得到用户提交源代码，input
                Json::Value in_value;                           // 创建一个Json类
                Json::Reader reader;                            // 提供读取服务的中间类，将字符串的内容读取到Json类中
                reader.parse(in_json, in_value);                // 通过中间类将 字符串的内容读取到 Json类中
                std::string code = in_value["code"].asString(); // 从Json类中获取用户代码

                // 2. 序列化：先将题目信息和代码填入Json类中，再将Json类序列化形成字符串
                Json::Value compile_value;                                // 创建一个Json类
                compile_value["code"] = code;                             // 从上面的Json类中获取用户代码
                compile_value["input"] = q.input;                         // 从Question结构体中获取input
                compile_value["answer"] = q.answer;                       // 从Question结构体中获取answer
                compile_value["cpu_limit"] = q.cpu_limit;                 // 从Question结构体中获取时间限制信息
                compile_value["mem_limit"] = q.mem_limit;                 // 从Question结构体中获取空间限制信息
                Json::FastWriter writer;                                  // 提供写入服务的中间类，将Json类的内容写入字符串中。
                std::string compile_string = writer.write(compile_value); // 通过中间类，将Json类的内容写入字符串中。

                // 差错处理: 选择的主机可能已经离线，无法完成请求，所以要循环选择主机，直到有主机完成判题请求。除非主机全部离线，直接跳出判题模块。
                while (true) // 循环选择主机，直到有主机完成判题请求。
                {
                    // 3. 调用负载均衡模块选择出负载最低的主机。
                    int id = 0;
                    Machine4 *m = nullptr;
                    if (!load_blance_.SmartChoice(&id, &m))
                    {
                        break; // 主机全部离线，直接跳出判题模块。
                    }
                    LOG(INFO) << " 选择主机成功, 主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 当前主机的负载是: " << m->Load() << "\n";

                    // 4. 创建一个httplib::Client对象，然后给选择出的主机发起http请求，得到结果
                    // 4.1 创建一个httplib::Client对象，作为客户端给服务端发送请求
                    Client cli(m->ip, m->port); // 传入服务端主机IP的地址和端口号(在conf文件中)实例化一个Client对象，作为客户端
                    m->IncLoad();               // 主机将进行编译运行服务，增加主机负载
                    // 4.2 给负责编译运行服务的主机发起/compile_and_run请求，并发送Json串，并得到返回结果
                    if (auto res = cli.Post("/compile_and_run4", compile_string, "application/json;charset=utf-8"))
                    {
                        Json::Value res_json;
                        Json::Reader res_rd;
                        res_rd.parse(res->body, res_json);
                        if (res_json["accept"] == "true")
                        {
                            int r = 0;
                            if ( (r = model_.AcceptOneQuestion(userid, &q)) != 0 && res->status == 200)
                            {
                                res_json["stat"] = 1;
                            }
                            else
                            {
                                res_json["stat"] = -1;
                            }
                        }
                        // 5. 将结果赋值给out_json（获取编译运行内容进行判题）
                        if (res->status == 200) // 返回的状态码为200时才表示响应完全成功
                        {
                            *out_json = res_json.toStyledString(); // 负责编译运行服务的主机返回的结果
                            m->DecLoad();          // 主机完成服务，减少主机负载
                            LOG(INFO) << "请求编译和运行服务成功..."
                                      << "\n";
                            break; // 有主机完成服务，跳出循环选择主机
                        }
                        m->DecLoad(); // 主机未完成服务，减少改主机负载，再次循环寻找主机完成服务。
                    }
                    else
                    {
                        // 发送请求失败，证明当前主机已经关闭，使用OfflineMachine()离线不能调用的主机
                        LOG(ERROR) << " 当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 可能已经离线"
                                   << "\n";
                        load_blance_.OfflineMachine(id); // 离线不能调用的主机
                        load_blance_.ShowMachines();     // 打印当前在线主机下标列表和离线主机下标列表，为了更好的调试
                    }
                }
            }
            else
            {
                Json::Value res;
                res["stat"] = 0;
                *out_json = res.toStyledString();
            }

            std::cout << *out_json << std::endl;
        }
    };
}